﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Pipes.Sgml;
using System.Diagnostics;
using ExCSS;
using ExCSS.Model;

namespace CssInliner
{
  public class Inliner
  {
    public string MoveCssInline(string htmlInput, bool removeStyleElements)
    {
      return MoveCssInline(htmlInput, removeStyleElements, null);
    }
    public string MoveCssInline(string htmlInput, bool removeStyleElements, Uri basePath)
    {
      using (var reader = new System.IO.StringReader(htmlInput))
      {
        var web = new System.Net.WebClient();
        var sgmlReader = new Pipes.Sgml.SgmlReader();
        sgmlReader.DocType = "HTML";
        sgmlReader.WhitespaceHandling = WhitespaceHandling.All;
        sgmlReader.CaseFolding = CaseFolding.ToLower;
        sgmlReader.InputStream = reader;
        sgmlReader.SimulateHtmlNode = false;
        sgmlReader.StripDocType = false;

        var doc = new XmlDocument();
        doc.Load(sgmlReader);

        var css = new StringBuilder();
        var nodes = doc.SelectNodes("//style");
        foreach (XmlNode node in nodes)
        {
          css.AppendLine(node.InnerText);
          if (removeStyleElements) node.ParentNode.RemoveChild(node);
        }
        nodes = doc.SelectNodes("//link[@rel='stylesheet' and (not(@media) or @media='' or @media='screen')]");
        string cssPath;
        foreach (XmlNode node in nodes)
        {
          cssPath = (basePath == null ? node.Attributes["href"].Value : new Uri(basePath, node.Attributes["href"].Value).ToString());
          css.AppendLine(web.DownloadString(cssPath));
          if (removeStyleElements) node.ParentNode.RemoveChild(node);
        }

        // Add a tbody tag to tables where needed
        nodes = doc.SelectNodes("//table");
        XmlElement tBody;
        bool inBody;
        int i;
        foreach (XmlNode node in nodes)
        {
          i = 0;
          tBody = null;
          inBody = false;
          while (i < node.ChildNodes.Count)
          {
            if (node.ChildNodes[i].Name == "tr" && !inBody) {
              if (tBody == null) tBody = doc.CreateElement("tbody");
              tBody.AppendChild(node.ChildNodes[i]);
              inBody = true;
            } else if ((node.ChildNodes[i].Name == "thead" || node.ChildNodes[i].Name == "tbody" || node.ChildNodes[i].Name == "tfoot") && inBody) {
              if (tBody != null) node.InsertBefore(tBody, node.ChildNodes[i]);
              tBody = null;
              inBody = false;
              i+=2;
            } else if (inBody) {
              tBody.AppendChild(node.ChildNodes[i]);
            } else {
              i++;
            }
          }
          if (tBody != null) node.AppendChild(tBody);
        }

        var parser = new Parser();
        var stylesheet = parser.Parse(css.ToString());
        IEnumerable<XmlNode> elementsStyle;
        IEnumerable<Property> inlineStyles;
        StyleSheet inlineSheet;
        string currStyle;
        string selector;

        foreach (var rs in stylesheet.Rulesets)
        {
          selector = rs.Selector.ToString();
          foreach (var s in selector.Split(','))
          {
            try
            {
              elementsStyle = doc.DocumentElement.QuerySelectorAll(s);
              foreach (XmlElement elem in elementsStyle)
              {
                currStyle = elem.GetAttribute("style");
                inlineStyles = Enumerable.Empty<Property>();
                if (!string.IsNullOrEmpty(currStyle))
                {
                  inlineSheet = parser.Parse("#a{" + currStyle + "}");
                  if (inlineSheet.Rulesets.Any())
                  {
                    inlineStyles = inlineSheet.Rulesets[0].Declarations;
                  }
                }
                elem.SetAttribute("style", MergeStyles(rs.Declarations, inlineStyles));
              }
            }
            catch (FormatException)
            {
              //Debug.Print("{0}  ->  {1}", rs.Selector, ex.Message);
            }
          }
        }
        return doc.OuterXml;
      }
    }
    private string MergeStyles(IEnumerable<Property> global, IEnumerable<Property> inline)
    {
      var globalList = (global == null ? new List<Property>() : global.ToList());
      var inlineList = (inline == null ? new List<Property>() : inline.ToList());

      if (globalList.Count < 1 && inlineList.Count < 1)
      {
        return string.Empty;
      }
      else 
      {
        List<Property> finalDecl;
        if (globalList.Count < 1) 
        {
          finalDecl = inlineList;
        }
        else if (inlineList.Count < 1)
        {
          finalDecl = globalList;
        }
        else
        {
          finalDecl = new List<Property>();
          globalList.Sort((x, y) => x.Name.CompareTo(y.Name));
          inlineList.Sort((x, y) => x.Name.CompareTo(y.Name));
          var globalPtr = 0;
          var inlinePtr = 0;
          while (globalPtr < globalList.Count && inlinePtr < inlineList.Count)
          {
            switch (globalList[globalPtr].Name.CompareTo(inlineList[inlinePtr].Name))
            {
              case -1:
                finalDecl.Add(globalList[globalPtr]);
                globalPtr++;
                break;
              case 1:
                finalDecl.Add(inlineList[inlinePtr]);
                inlinePtr++;
                break;
              default:
                finalDecl.Add(inlineList[inlinePtr]);
                globalPtr++;
                inlinePtr++;
                break;
            }
          }
          while (globalPtr < globalList.Count)
          {
            finalDecl.Add(globalList[globalPtr]);
            globalPtr++;
          }
          while (inlinePtr < inlineList.Count)
          {
            finalDecl.Add(inlineList[inlinePtr]);
            inlinePtr++;
          }
        }
        return BuildStyle(finalDecl);
      }
    }
    private string BuildStyle(IEnumerable<Property> style)
    {
      var builder = new StringBuilder();

      foreach (var d in style)
      {
        if (builder.Length > 0) builder.Append(';');
        builder.Append(d.ToString());
      }

      return builder.ToString();
    }

    //public static void Test(string path)
    //{
    //  var html = System.IO.File.ReadAllText(path);
    //  var result = new Inliner().MoveCssInline(html, true);
    //}
  }
}
